package fr.lelouet.stress

import org.hyperic.sigar.Sigar
import org.slf4j.Logger
import org.slf4j.LoggerFactory



class CPUStress extends AStressable {

	private static final Logger logger = LoggerFactory.getLogger(CPUStress.class)

	/** number of cycles to pass on computing before giving back hand to the main program*/
	long loopSize=10

	/** only used to force processor action*/
	protected def assigned=0

	protected void runningLoop() {
		loopSize.times { assigned++ }
		Thread.sleep 1
	}

	/** do some stress actions. The stresses start at a zero lvl, and is multiplied per 2 each time. it generates one stresser thread per CPU **/
	static void main(args) {
		def paramPeriod = 3000
		CPUStress stress = new CPUStress()
		logger.info("starting calibration")
		def params = stress.calibrateLoopSize()
		params.each{
			logger.info(" $it is a good calibration value")
		}
		logger.info("starting burning, period is $paramPeriod")
		new Sigar().getCpuList().size().times {
			Thread.start { stress.run()
			} }
		while(true) {
			logger.info(" new loop")
			for(Object o : params) {
				logger.info (" parametring with $o")
				stress.configure(o)
				Thread.sleep paramPeriod
			}
		}
		stress.stop()
	}

	/** the minimum power of 2 to start bench with*/
	int minBenchPow = 0

	/** minimum idle CPU value to consider that a parameter is interesting while calibrating*/
	double minIdle=0.05

	/** maximum idle CPU value to consider that a parameter is interesting while calibrating*/
	double maxIdle = 0.85

	/** while calibrating, start accepting all data after that many parameters have given an accepted idleness, and stop accepting all data after that many parameters have not been accepted*/
	int successiveChecksToAccept = 3

	/**
	 * test several values for the {@link #loopSize} parameter<br />
	 * start from 2^ #minBenchPow , multiplying tested value by 2<br />
	 * each value is tested with {@link Sigar} to get the idleness of CPU under such a load<br />
	 * The calibration stops after #successiveChecksToAccept successive parameters have been &lt; #maxIdle , and  then #successiveChecksToAccept have been &lt; #minidle
	 * @return a list of parameters that have meaning for generating different CPU load.
	 * Those parameters can be given to {@link #loopSize}*/
	public List<Long> calibrateLoopSize(long delay_ms=1000) {
		Sigar sigar= new Sigar()
		int nbCPU
		try {
			nbCPU=sigar.getCpuList().size()
		} catch(UnsatisfiedLinkError er) {
			logger.error("cannot start the sigar. ensure the sigar API is installed. ubuntu amd64 script (as root): cd \$(mktemp -d);\
wget \"http://downloads.sourceforge.net/project/sigar/sigar/1.6/hyperic-sigar-1.6.4.zip?r=&ts=1294065864&use_mirror=netcologne\"\
; unzip *; cp hyperic-sigar-1.6.4/sigar-bin/lib/libsigar-amd64-linux.so /usr/lib/")
			throw er
		}
		CPUStress stress = this
		Thread.start { stress.run() }
		long param = Math.pow(2, minBenchPow) as Long
		/** the params used to stress the CPU*/
		List params = []
		/** the visible idleness of the CPU , for each param*/
		List idleness = []
		param = calibrateTillNoIdle(params, idleness, param, sigar, delay_ms, nbCPU)
		stopNow()
		return analyseGoodParams(params, idleness)
	}

	/** stress with parameter multiplied by 2 each time. Stop when idleness < minidle three times in a row*/
	protected long calibrateTillNoIdle(List params, List idleness, long param, Sigar sigar, long delay_ms, int nbCPU, int nbpass=4) {
		logger.info "   looking for low idleness; nbcpu=$nbCPU"
		int nbfail=0
		while(nbfail<successiveChecksToAccept) {
			setLoopSize param
			def idle=0
			nbpass.times {
				Thread.sleep(delay_ms/nbpass as Long)
				def idleNow = sigar.getCpuPerc().getIdle()
				if(idleNow>idle) {
					idle=idleNow
				}
			}
			def normalized = normalizeIdle(idle,nbCPU)
			params[params.size()] = param
			idleness[idleness.size()] = idle
			if (normalized>=minIdle) {
				nbfail=0
			} else {
				nbfail++
			}
			param *=2
		}
		return param
	}

	/** return a list of the interesting parameters, according to the results they gave.*/
	protected List analyseGoodParams(List params, List idleness) {
		List ret = [0]
		def normalized = normalizeList(strictlyDecreasing(idleness))
		params.size().times{logger.debug ("parameter {} is {}, it gives {}, normalized to {}", it,params[it], idleness[it], normalized[it] ) }
		int firstIndex= normalized.findIndexOf {it<=0.95} -1
		if(firstIndex<0) {
			firstIndex=0
		}
		int lastIndex = normalized.findLastIndexOf {it>0.05} +2
		if(lastIndex>normalized.size()-1) {
			lastIndex=normalized.size()-1
		}
		return params.subList (firstIndex, lastIndex)
		return ret
	}

	/** normalize a list of double so that the min is 0, the max is 1*/
	public static List normalizeList(List input) {
		if(input.size()<2) {
			return input
		}
		def min = Double.POSITIVE_INFINITY
		def max = Double.NEGATIVE_INFINITY
		input.each{
			if(it<min) {
				min=it
			}
			if(it>max) {
				max=it
			}
		}
		def normalized = []
		input.eachWithIndex {it, idx ->normalized[idx] = (it-min)/(max-min) }
		return normalized
	}

	/** enforce a strict decreasing in a list l : if one l[i]=a is lower than an item l[j]=b, i<j and a<b, then it is set to l[i]=b */
	public static List strictlyDecreasing(List input) {
		List ret = []
		double min = input.last()
		input.reverseEach {
			if(it<min) {
				ret.add(min)
			} else {
				min = it
				ret.add(it)
			}
		}
		return ret.reverse()
	}

	/** normalize a full CPU load */
	public static double normalizeIdle(double cpuIdle, int nbCPU) {
		def normalized = 1-(1-cpuIdle)*nbCPU
		if(normalized<0) {
			normalized=0
		}
		if(normalized>1) {
			normalized=1
		}
		logger.debug("  normalizing idle $cpuIdle  with $nbCPU CPUs to $normalized")
		return normalized
	}

	public List<Object> calibrate() {
		List<Object> ret = new ArrayList<Object>()
		ret.addAll(calibrateLoopSize())
		return ret
	}

	public boolean applyConfiguration( configuration ) {
		try {
			Long val = configuration as Long
			setLoopSize val
			return true
		} catch (Exception e) {
			logger.info ("cannot configure {} with configuration {}", this.getClass(),configuration)
			return false;
		}
	}
}
